/**
 * 
 */
package com.xucia.jsponic.remote;
 
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;

import com.xucia.jsponic.data.GlobalData;
import com.xucia.jsponic.data.Identification;
import com.xucia.jsponic.data.JsonPath;
import com.xucia.jsponic.data.ObjectId;
import com.xucia.jsponic.data.ObjectPath;
import com.xucia.jsponic.data.Persistable;
import com.xucia.jsponic.data.PersistableImpl;
import com.xucia.jsponic.datasource.DataSource;
import com.xucia.jsponic.js.RemoteScript;
import com.xucia.jsponic.remote.AliasIds.AliasHandler;
import com.xucia.jsponic.remote.ClientConnection.IndividualRequest.Path;
import com.xucia.jsponic.rpc.RPCMessage;
import com.xucia.jsponic.rpc.RPCResponse;
import com.xucia.jsponic.rpc.RPCall;
import com.xucia.jsponic.security.ObjectAccessDeniedException;
import com.xucia.jsponic.security.User;
import com.xucia.jsponic.util.JsponEncoding;
import com.xucia.jsponic.util.StrongSoftSyncHashMap;

public class ClientConnection { 
	public static final boolean LOG_ALL_OUTPUT = false;
	public static Set<String> JSPON_RESERVED_WORDS = new HashSet<String>();
	static SimpleDateFormat ISO_SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
	static {
		TimeZone utc = TimeZone.getTimeZone( "UTC" );
        ISO_SDF.setTimeZone ( utc );
	}
	static private String dateToIso(Date date) {
        return ISO_SDF.format( new Date() );
        // convert from milliseconds to centiseconds
        // by chopping off last digit, and reappending the decorative Z.
        //return milliformat.substring( 0, 22 ) + 'Z' ;
	}
	static int pageSize = 51; 
	static {
		JSPON_RESERVED_WORDS.addAll(Arrays.asList(new String[] {"id","ready","aliasId","update"})); // basis is too, but we store that, it goes through as stored
		Logger.getLogger(ClientConnection.class.toString()).setLevel(Level.WARNING);
	}
//	Map<String,String> javaScriptDebuggingPastRequests; // this is to store the past requests that have been for javascript venkman debugging to work properly
	//public boolean isSafari = false;
	private boolean debugMode = false; 
	public boolean safeMode = false;
	static JsponSerializer jsponSerializer;
	public static void setJsponSerializer(JsponSerializer jsponSerializer) {
		ClientConnection.jsponSerializer = jsponSerializer;
	}
	static ClientTransformationPlugin clientTransformationPlugin;
	static Map<String,Set<PrioritizedObject>> prefetchTable = new StrongSoftSyncHashMap<String,Set<PrioritizedObject>>(10); // A map of id requests to a map of how long the subsequent requests have taken and a the value that was requested
	List<TimedRequest> recentRequests = new ArrayList<TimedRequest>(); // the recent requests and the time at which the request was made
	public IndividualRequest getIndividualRequest(HttpServletRequest httpRequest) {
		return new IndividualRequest(httpRequest);
	} 
	public static long DELAY_REQUEST_THRESHOLD = 7000;
	public static long CONSIDERATION_THRESHOLD = 2 * DELAY_REQUEST_THRESHOLD; 
	public static long SEPARATE_REQUEST_THRESHOLD = 4000;
	public static final int STRING_TIME_WEIGHT = 2; // The higher this value the more restrictive about including long strings
	public static long HISTORIC_FACTOR = 5;
	static Pattern FUNCTION_COMPRESSION_PATTERN = Pattern.compile("\\xEA([^\\xEA-\\xEF]*)([\\xEA-\\xEF])");
	public boolean prefetchEnabled = true;
	private Map<String,PostResponse> postResponses = new HashMap<String,PostResponse>();
	static class PostResponse {
		public StringWriter writer = new StringWriter();
		public JSONObject headers = new JSONObject();
		boolean finished = false;
	}
	public static Object NotReadyYet = new Object();
	private static Scriptable arrayPrototype = ScriptableObject.getClassPrototype(GlobalData.getGlobalScope(),"Array");
	private static Scriptable objectPrototype = ScriptableObject.getClassPrototype(GlobalData.getGlobalScope(),"Object");
	public static class TimedRequest {

		long requestTime;
		String requestedObject;
		public TimedRequest(String requestedObject, long requestTime) {
			this.requestTime = requestTime;
			this.requestedObject = requestedObject;
		}
	}
	public static class PrioritizedObject implements Comparable<Object> {
		float requestDelayAverage = DELAY_REQUEST_THRESHOLD;
		Persistable value;
		Object field;
		void recentRequestDelay(long delay) {
			requestDelayAverage = (requestDelayAverage * HISTORIC_FACTOR + delay) / (HISTORIC_FACTOR + 1); 

		}
		public int compareTo(Object o) {
			if (requestDelayAverage > ((PrioritizedObject)o).requestDelayAverage)
				return 1;
			else if (requestDelayAverage == ((PrioritizedObject)o).requestDelayAverage)
				try {
					return value.toString().compareTo(((PrioritizedObject)o).value.toString());
				}
				catch (NullPointerException e) {
					return -1;
				}
			else 
				return -1;
		}
		
	}
	private String authorization;
	public static class ObjectFetchRequest {
		Persistable object;
		List<String> fields;
		void addField(String field) {
			if (fields == null) 
				fields = new ArrayList<String>();
			fields.add(field);
		}
		@Override
		public int hashCode() {
			return object.hashCode();
		}
	}
	//TODO: This should be it's own class, not inner
	public class IndividualRequest {
		Set<String> alreadyFulfilled = new HashSet<String>(100);
		List<String>[] accessLevels = new List[6];
		//List<Entity> returnList = new ArrayList();

		//List<Id> alreadyDeclared = new ArrayList();
        String aliasId;
        String aliasRealId;
        int indexRequested = 0;
        String requestedIndexId = null;
        HttpServletRequest httpRequest;
		IndividualRequest(String locale) {
			this.locale = locale;
		}
		IndividualRequest(HttpServletRequest httpRequest) {
			this.httpRequest = httpRequest;
		}       
		Set<Persistable> parentSet = new HashSet<Persistable>();
		String locale;
		Set<PrioritizedObject> prefetchSet;
		public ClientConnection getConnection() {
			return ClientConnection.this;
		}
		boolean shouldPrefetch(Persistable object) { // TODO: This must be FASTER!!!! we could use a hash to point to the entries
			if (prefetchSet != null) {
				for (PrioritizedObject prefetchObject : prefetchSet) {
					//Logger.getLogger(ClientConnection.class.toString()).info("prefetching (maybe): " + prefetchObject.value + " " + prefetchObject.field + " " + prefetchObject.requestDelayAverage);
					if (prefetchObject.value == object)
						return true;
					if (prefetchObject.requestDelayAverage > DELAY_REQUEST_THRESHOLD)
						return false;;
				}
			}
			return false;
		}
		List<PrioritizedObject> getPrefetchs(Persistable object) { // TODO: This must be FASTER!!!! we could use a hash to point to the entries
			List<PrioritizedObject> prefetchs = new ArrayList<PrioritizedObject>();
			if (prefetchSet != null) {
				for (PrioritizedObject prefetchObject : prefetchSet) {
					//Logger.getLogger(ClientConnection.class.toString()).info("prefetching (maybe): " + prefetchObject.value + " " + prefetchObject.field + " " + prefetchObject.requestDelayAverage);
					if (prefetchObject.value == object)
						prefetchs.add(prefetchObject);
					if (prefetchObject.requestDelayAverage > DELAY_REQUEST_THRESHOLD)
						break;
				}
			}
			return prefetchs;
		}
		public boolean codeEnabled = true;
		DataSource requestedSource;
		String requestedSubPath;
		public Object requestData(String requestedPath) {
			synchronized (ClientConnection.this) {
					long ifModifiedSince = 0;
					if (httpRequest != null) {
						String changesSinceString = httpRequest.getParameter("changesSince");
						if (changesSinceString != null)
							ifModifiedSince = Long.parseLong(changesSinceString);
						locale = httpRequest.getHeader("Accept-Language");
						if (locale == null)
							locale = httpRequest.getParameter("Accept-Language");
					}
					//TODO: We need to check the Referer header and then make a determination if this is requested an object that 
					//we have cached (from the import maps) that we should return an object with redirection to local like local://id.
//					String code = request.getParameter("code");
	//				codeEnabled = code != null && ((code = code.toLowerCase()).startsWith("js") || code.startsWith("javascript"));				
					Persistable object; 
			        //Id requestedId = new Id(requestedObject); // removes the leading underscore
			        //requestedId.source = sourceURL;
					String field = null;
			        Object value = null; 
			        Logger.getLogger(ClientConnection.class.toString()).info("requestedId " + requestedPath);
			        AliasHandler aliasHandler = AliasIds.getAliasHandler(requestedPath);
			        if (aliasHandler != null) {
						object = aliasHandler.getObject();
						if (aliasHandler.getField() != null)
							field = aliasHandler.getField();
						aliasId = requestedPath;
						if (object == null) {
							object = new NativeObject();
							aliasRealId = object.getId().toString();
							return object;
						}
						aliasRealId = object.getId().toString();
					}
					else {
				        //requestedId.objectId = requestedObject;
						Identification id = Identification.idForString(requestedPath);
						if (id instanceof ObjectId || id instanceof JsonPath)
							object = ((Identification<Persistable>)id).getTarget();
						else {
							object = ((ObjectPath)id).getSecondToLastTarget();
							field = ((ObjectPath)id).getLastPath().toString();
							value = id.getTarget();
						}
				        requestedSource = id.getSource();
				        int lastSlash = id.subObjectId.lastIndexOf('/');
				        if (lastSlash > -1)
				        	requestedSubPath = id.subObjectId.substring(0,lastSlash+1);
					}
/*			        if (object instanceof Versionable && ifModifiedSince > 0 && ((Versionable) object).getLastModified().getTime() <= ifModifiedSince) {
			        	//output("[]");
			        	return object; 
			        }*/
					
			        /* This is the prefetch section */
			        if (field != null) { 
			        	if (field.matches("[0-9]+")) { // an index is being requested
			        		indexRequested = Integer.parseInt(field);
			        		field = null;
			        		requestedIndexId = requestedPath;
			        	}
			        }
			        // TODO: Only do prefetch analysis if it is lazy
				        long now = new Date().getTime();
				        recentRequests.add(new TimedRequest(requestedPath,now));
				        for (int i =recentRequests.size() -2; i >= 0;i--) {// Go back through the recent requests and see if there is any that need to be added to the prefetchTable
				        	TimedRequest request = recentRequests.get(i);
				        	long thisRequestTime = request.requestTime;
				        	if (now - thisRequestTime > CONSIDERATION_THRESHOLD) 
				        		break; 
				        	if (i==0 || thisRequestTime - recentRequests.get(i-1).requestTime > SEPARATE_REQUEST_THRESHOLD) {
				        		// go to the object that is the trigger
				        		//System.err.println("This was a trigger from " + recentRequests.get(i).requestedObject);
				        		Set<PrioritizedObject> prefetchObjects = prefetchTable.get(recentRequests.get(i).requestedObject);
				        		if (prefetchObjects == null) {
				        			prefetchObjects = new TreeSet<PrioritizedObject>(); 
				        			prefetchTable.put(recentRequests.get(i).requestedObject,prefetchObjects);
				        		} 
				        		boolean found = false;
				        		for (PrioritizedObject prefetchObject : prefetchObjects) {
				        			if (prefetchObject.value == object && (prefetchObject.field == field || (field != null && field.equals(prefetchObject.field)))) {
					        		//System.err.println("found and reprioritizing PrioritizedObject" + object);
				        				prefetchObjects.remove(prefetchObject);  // we need to take it out and put it back into the tree so it gets reordered 
				        				prefetchObject.recentRequestDelay(now - thisRequestTime);
				        				//System.err.println(prefetchObject.value.getId() + " " + prefetchObject.requestDelayAverage);
				        				prefetchObjects.add(prefetchObject);
				        				found = true;
				        				break;
				        			} 
				        		}
				        		if (!found) { 
				        			PrioritizedObject prefetchObject = new PrioritizedObject();
				        			prefetchObject.value = object;
				        			prefetchObject.field = field;
					        		for (PrioritizedObject firstObject : prefetchObjects) {
					        			if (firstObject.value == object) {
					        				if (firstObject.field == null)
					        					firstObject.field = new ArrayList<PrioritizedObject>();
					        				if (firstObject.field instanceof List)
					        					((List<PrioritizedObject>) firstObject.field).add(prefetchObject);
					        			}
					        		}				        			
				        			prefetchObject.recentRequestDelay(now - thisRequestTime + (value instanceof String ? ((String) value).length()*STRING_TIME_WEIGHT:0));
			        				//System.err.println(prefetchObject.value.getId() + " " + prefetchObject.requestDelayAverage);
				        			prefetchObjects.add(prefetchObject);
				        		} 
				        		break;
				        	} 
						}
					//System.err.println("Starting request handling for object "
						//	+ object.getId());
				//	output("[");
			        try {
			        	//if (field == null)
			        		//alreadyFulfilled.remove(object.getId()); // we should never be denied a request becaues it has already been fulfilled
		        		//output(requestObject(object,false,false,false,field!=null?new String[] {field}: null,aliasId));
						if (prefetchEnabled)  {
							prefetchSet = prefetchTable.get(requestedPath);
/*							if (prefetchSet != null)
								synchronized (prefetchSet){ 
									LinkedList<PrioritizedObject> removalList = new LinkedList();
									for (PrioritizedObject prefetchObject : prefetchSet) {
										//System.err.println("prefetching (maybe): " + prefetchObject.value + " " + prefetchObject.field + " " + prefetchObject.requestDelayAverage);
										if (prefetchObject.requestDelayAverage > DELAY_REQUEST_THRESHOLD)
											break;
										try {
											String requiredField = null;
											if (prefetchObject.field instanceof String)
												requiredField = (String)prefetchObject.field; 
//											boolean functionCode = GlobalData.FUNCTION_COMPILED_CODE_FIELD.equals(requiredField);
											if (requiredField == null) 
												output(requestObject(prefetchObject.value,false,false,true,null,null));
											else {
												Object prefetchValue = prefetchObject.value.get(requiredField);												
												 if (prefetchValue instanceof DataNotFoundInstance)
													 removalList.add(prefetchObject); // not found, it needs to be removed from the prefetch list
												 else
													 output(requestField(prefetchObject.value,requiredField,prefetchValue));
											}
										}
										catch (SecurityException se) {
											removalList.add(prefetchObject);
										}
										catch (DataException de) {
											removalList.add(prefetchObject);
										}
									}
									prefetchSet.removeAll(removalList);
								}*/
							
						}
						else { // we are not in prefetchMode, which means that we are probably doing a server side rendering and this is our chance to lower the priority of objects
						// lower the priority of objects so that will move out the prefetch if they are not fetched later
							prefetchSet = prefetchTable.get(requestedPath);
							if (prefetchSet != null)
								synchronized (prefetchSet){
					        		for (PrioritizedObject prefetchObject : prefetchSet) {
				        				prefetchObject.recentRequestDelay((long) (DELAY_REQUEST_THRESHOLD * 1.2));
					        		}
								}
						}
/*						while (!parentSet.isEmpty()) {
							Set<DataObject> parentSetToIterate = new HashSet(parentSet);
							for (DataObject parent : parentSetToIterate) {
								try {
									output(requestObject(parent,false,false,false,null,null));
									parentSet.remove(parent);
								}
								catch (Exception e) {
									e.printStackTrace();
								}
							}
						}*/
					} 
					catch (RuntimeException e) {
						e.printStackTrace();
					}
//					output("0]");//must have an dummy item to come after the last comma

				if (LOG_ALL_OUTPUT)
					Logger.getLogger(ClientConnection.class.toString()).info(outputSoFar.toString());
				if (value != null)
					return value;
				return object;
			}
		}
		
		Map<ObjectId,ObjectPath> pathStyleRefs = new HashMap();
		class Path {
			Path parent;
			Object key;
			ObjectId objId;
			public Path(ObjectId objId, Path parent, Object key) {
				super();
				this.objId = objId;
				this.parent = parent;
				this.key = key;
			}
			ObjectPath toObjectPath() {
				List<Object> paths = new ArrayList<Object>();
				Path path = this;
				ObjectId objId=null;
				while (path != null) {
					paths.add(0,path.key);
					objId = path.objId;
					path = path.parent;
				}
				return new ObjectPath(objId,paths);
			}
		}
		int depth = -1;
		void writeNewLine(StringBuffer buffer, boolean needComma) {
			buffer.append(needComma ? ",\n":"\n");
			for (int i = 0; i < depth; i++)
				buffer.append("\t");
		}
		String requestObject(Persistable obj,boolean propertyValue,boolean prefetching, Path path) {
			/*
			 * String declaration; if (useReference) declaration =
			 * writeObjectReference(obj,false); else declaration =
			 * writeSourceObjectReference(obj,false); if (declaration.length() >
			 * 0) output(declaration + ";\n");
			 * //returnList.add(instance.data());
			 */
			//System.err.println("request Object: " + obj.getId());
			StringBuffer buffer = new StringBuffer(); 
			try { 
				depth++;
				ObjectId objId = obj.getId();
				boolean persisted = objId.isPersisted();
				String idString = objId.toString(requestedSource,requestedSubPath);
				

				if ((alreadyFulfilled.contains(objId) && persisted) || objId.isLocal()) { // no child mods are only allowed here if they are a property value
					if (propertyValue) {
						if (pathStyleRefs.containsKey(objId))
							idString = pathStyleRefs.get(objId).toString(requestedSource,requestedSubPath);
/*						if (codeEnabled && (doFunction || obj.get(GlobalData.FUNCTION_COMPILED_CODE_FIELD) instanceof String))
						{ 
						else*/
							buffer.append("{\"$ref\":\"" + idString + "\"}");
					}
				}
				else {
					alreadyFulfilled.add(objId.toString());
					boolean commaNeeded = true;
					String id = objId.toString();
					if (id == null) {
						if (aliasId != null) 
							buffer.append("{\"id\":" + JSONObject.quote(aliasId));						
					}
					else {
						if (persisted || obj.getAcl() != null) {// if there is security defined we should include the id so that the security can be defined for the client
							buffer.append("{\"id\":\"" + idString + "\"");
						}
						else {
							buffer.append("{");
							commaNeeded = false;
						}
					}
					Object basis = obj.getPrototype();
					if (basis != null && basis != objectPrototype && basis != arrayPrototype && !(obj instanceof Function)) {
						writeNewLine(buffer,commaNeeded);
						buffer.append("\"basis\":" + getValueString(basis, true));
						commaNeeded = true;						
					}
/*					Object basis = obj.get(GlobalData.BASIS_FIELD);
					if (basis instanceof DataObject) {// must always send the basis first
						output(requestObject((DataObject) basis,false,false,false,null,obj));
					}
*///					if (obj instanceof Versionable)
	//					buffer.append(sendField("version", ((Versionable) obj).getVersionNumber()));
					/*
					 * if (basis != null) { buffer.append(",\n");
					 * sendField(obj,"proto",basis.getId()); }
					 */
					
					addAccessLevel(idString, obj.getAccessLevel());
					List<PrioritizedObject> prefetchs = getPrefetchs(obj);
					boolean stringKeys= false;
					if (obj instanceof Function) {
						Entry<String,String> entry = clientTransformationPlugin.augmentJsponForFunction((Function) obj,debugMode);
						writeNewLine(buffer,commaNeeded);						
						buffer.append(JSONObject.quote(entry.getKey()) +  ":" + JSONObject.quote(entry.getValue()));
						commaNeeded = true;
					}
					
					Object lazy = obj.get("__lazy__");
					
					for (String key : obj.keySet()) {
						try {
							if (key.startsWith(":")) 
							{}							
							else {
								if (JSPON_RESERVED_WORDS.contains(key))
									key = JSONObject.quote("public::" + key);
								try {
									boolean required= jsponSerializer.isRequired(lazy) || "structure".equals(key); // TODO: allow specified properties
									//TODO: Structure needs to also cause structure children to always be loaded
									if (!required)
										for (PrioritizedObject prefetch : prefetchs)
											if (key.equals(prefetch.field))
												required = true;
									if (!GlobalData.PARENT_FIELD.equals(key)) {
//										didParent = true; // TODO: Get rid of parent in the data
										Object value = PersistableImpl.getSlotValue(obj,key);
	//									if (!(GlobalData.PARENT_FIELD.equals(key) && value == parentValue && parentValue != null)) { // if parent value is null we should write it out
											if (GlobalData.FUNCTION_COMPILED_CODE_FIELD.equals(key)) {
												
					//							buffer.append(",\n\"psv15\":" + functionDecompress((String) value));
											}
											else {
												writeNewLine(buffer,commaNeeded);
												buffer.append(JSONObject.quote(key) + ":" + getValueString(value, !persisted || required,new Path(objId,null,key)));
												commaNeeded = true;
											}
											stringKeys = true;
		//								}
									}
								}
								catch (ObjectAccessDeniedException e) {
									writeNewLine(buffer,commaNeeded);
									buffer.append(JSONObject.quote(key) + ":{ready:0,access:0}");
									commaNeeded = true;
								}
								
							} 
						} catch (Exception e) {
							e.printStackTrace(); 
						}
					}
					StringBuffer arrayBuffer = null; 
					if (obj instanceof List) {
						arrayBuffer = new StringBuffer();
						try { 
								writeNewLine(buffer,commaNeeded);
								buffer.append("\"array\":"); 
								arrayBuffer.append("[");
								commaNeeded= false;
								int startingIndex = obj.getId().equals(requestedIndexId) ? indexRequested : 0;
								int size = ((List<Object>) obj).size();
								for (int i = startingIndex; i < size; i++) {
									Object item = ((List<Object>)obj).get(i);
									writeNewLine(arrayBuffer, commaNeeded);
									commaNeeded = true;
									try {
										arrayBuffer.append(getValueString(item, true,new Path(stringKeys ? objId : null, stringKeys ? null : path,i)));
									}
									catch (Exception e) {
										e.printStackTrace();
										arrayBuffer.append("{\"id\":\"" + ((Persistable) item).getId().toString(requestedSource,requestedSubPath) + "\"}");
									}
									if (i>=pageSize + startingIndex -1) {
										String ready = "\"" + startingIndex + "-" + (pageSize + startingIndex -1) + "\"";
										break;
									}
										
									//arrayBuffer.append(getValueString(item, false));
								}
								if (startingIndex > 0) {
									String ready = "\"" + startingIndex + "-\"";
								}
						}
						catch (ObjectAccessDeniedException e) {
							Logger.getLogger(ClientConnection.class.toString()).info(e.getMessage());
						}
						catch (Exception e) {
							e.printStackTrace(); 
						}
						finally {
							arrayBuffer.append("]");
						}
					}
					//if (appendField != null) 
						//buffer.append(appendField);
					if (arrayBuffer != null) {
						if (stringKeys) { 
							buffer.append(arrayBuffer);
							writeNewLine(buffer, false);
							buffer.append("}");
						}
						else {
							if (path != null)
								pathStyleRefs.put(objId,path.toObjectPath());
							
							buffer = arrayBuffer;
						}
					}
					else {
						writeNewLine(buffer, false);
						buffer.append("}");
					}
					if (!propertyValue)
						buffer.append(",\n\n");
					
				}
			} 
			catch (RuntimeException e) {
			
				e.printStackTrace();  
				if (prefetching)
					throw e;
				else
					buffer.append("{\"$ref\":\"" + obj.getId().toString(requestedSource,requestedSubPath) + "\",access:0},\n\n");
				//if (!propertyValue)
					//buffer.append(",\n\n"); 
			}
			finally { 
				depth--;
			}
			return buffer.toString(); 
		}

		StringBuffer outputSoFar = new StringBuffer();
 


		/*public void sendDeclaration(Id instance, Id basis) {
			output("{id:\"" + instance + "\"}\n");
		}*/
		public static final int MAX_SIZE_AUTO_SEND_FIELD = 50;
		public String getValueString(Object value, boolean unconditionally) {
			return getValueString(value, unconditionally,null);
		}
		//TODO: This should not be an entry point
		public String getValueString(Object value, boolean unconditionally, Path path) {//TODO: make the second parameter unnecesssary

			String valueString;
			/*if (value instanceof XMLElement)
				value = ((XMLElement) value).getNewConversion();*/
			if (value instanceof String) {
				if (!unconditionally && ((String) value).length() > MAX_SIZE_AUTO_SEND_FIELD)
					return "{ready:0}";
				else
					return JSONObject.quote((String) value);
			} else if (value instanceof Persistable) {
				try {
					if (unconditionally || shouldPrefetch((Persistable) value) || !((Persistable) value).getId().isPersisted())
						 return requestObject((Persistable) value, true, true, path);
					else {
						ObjectId objId = ((Persistable) value).getId();
						String idPrefix = "{\"$ref\":\"" + objId.toString(requestedSource,requestedSubPath);
						if (value instanceof Function)
							return idPrefix + "\",\"function\":{\"ready\":0}}";
						else if (value instanceof List)
							return idPrefix + "\",\"array\":{\"ready\":0}}";
						else
							return idPrefix + "\"}";
					}
					
				}
				catch (ObjectAccessDeniedException e) {
					Logger.getLogger(ClientConnection.class.toString()).info(e.getMessage());
					valueString = "{\"id\":\""+ ((Persistable) value).getId().toString(requestedSource,requestedSubPath) + "\",\"ready\":1}";
				}
			} 
			else if (value instanceof Number || value instanceof Boolean)
				return value.toString();
			else if (value instanceof Date) {
/*				if (standardsMode)
					return "\"" + ISO_SDF.format((Date)value).replace(' ', 'T') + "Z\"";//return "\"@" + ((Date) value).getTime() + "@\"";
				else*/
					return "\"" + dateToIso((Date)value) + "\"";
			}
			else if (value == null)
				return "null";
			else if (value instanceof RemoteScript) {
				valueString = ((RemoteScript)value).getScriptText();
			}
			else if (value == NotReadyYet)
				valueString = "{\"ready\":0}";
			else if (value instanceof ObjectId) 
				throw new RuntimeException("can not directly handle objectId");
			else {  
				if (value == Scriptable.NOT_FOUND)
					return "undefined"; 
				Logger.getLogger(ClientConnection.class.toString()).info("Can not output an object of type "
						+ value.getClass() + " to JSON");
				valueString = "\"DataNotFoundInstance\""; 
			} 
			return valueString; 
		} 

		public String sendField(Object key, Object value) {
			return "\n\"" + key.toString() + "\":" + getValueString(value, false);
		}
		public String functionDecompress(String code) {
			Matcher m = FUNCTION_COMPRESSION_PATTERN.matcher(code);

			StringBuffer sb = new StringBuffer(); 
			while (m.find())
			{ 
				String type = m.group(2); 
				if (type.equals("\u00eb")) {
					m.appendReplacement(sb,"(){with(\\_frm(this,arguments," + m.group(1) + ")){while(1){switch(\\_cp){case 0:");
				}
				else if (type.equals("\u00ec")) {
					String cp = m.group(1);
					m.appendReplacement(sb,"\\_cp=" + cp + ";case " + cp + ":if((\\_r.v" + cp + "=");
				}
				else if (type.equals("\u00ed")) {
					m.appendReplacement(sb,")===\\_S){return \\_s();}");
				}
				else if (type.equals("\u00ee")) { 
					m.appendReplacement(sb,"return;case -1:return \\_s()}}}}");
				} 
				else
					throw new RuntimeException("Invalid compression string");
			} 
			m.appendTail(sb);
			code = sb.toString();
//			if (isSafari) // If it is safari we must remove the named function and make it anonymous
	//			code = "\"function\"" + code.substring(code.indexOf("("));
			if (safeMode)
				return JSONObject.quote(code);
			else
				return code;
		}  
		
		/* This writes out any objects that need updating.  This is to output a value in the JSON object */
		@Deprecated // TODO: remove this
		public String outputWaitingData() { 
			synchronized (clientObjectsChangeIdNeeded) {
				StringBuffer output = new StringBuffer();

				output.append("{data:");
				output.append(JsponEncoding.makeJSONArray(clientObjectsChangeIdNeeded.entrySet(), new JsponEncoding.ItemHandler() {
					public String handleItem(Object object) {
						Map.Entry<String,Persistable> clientObject = (Map.Entry<String,Persistable>) object; 
						Persistable obj = clientObject.getValue();
						addAccessLevel(obj.getId().toString(),obj.getAccessLevel());
						return "{aliasId:\"" + clientObject.getKey() + "\",id:\"" + obj.getId() +"\"}"; 
					}
				}));
				output.append("}");
				clientObjectsChangeIdNeeded.clear();
				return output.toString();
			}
		}
		public String getWaitingRPCs() {
			RPCMessage message = dispatchNextMessages();
			if (message instanceof RPCall) {
				return "{\"id\":" + JSONObject.quote(message.getId()) 
				+ ",\"object\":" + getValueString(((RPCall) message).getTarget(),true) + ",\"method\":" 
				+ JSONObject.quote(((RPCall) message).getFunctionName()) + ",\"params\":" +
				JsponEncoding.makeJSONArray(Arrays.asList(((RPCall) message).getParameters()), new JsponEncoding.ItemHandler() {
					public String handleItem(Object object) {								
						return getValueString(object, true); 
					}							
				}) + "}";
			}
			else if (message instanceof RPCResponse) {
				return "{\"id\":" + JSONObject.quote(message.getId()) 
				+ ",\"result\":" + getValueString(((RPCResponse) message).getResult(),true)
				+ ",\"error\":" + getValueString(((RPCResponse) message).getError(),true) + "}";
				
			}
			else throw new RuntimeException("Unknown RPC message: " + message);
		}
	    public void setLogin(User newLogin) {
	        if (session != null) {
	        	session.setAttribute(User.class.toString(),newLogin);
	        }
	    }
	    public HttpSession getSession() {
	    	return session;
	    }
	    public void sendMessage(RPCMessage message) {
	    	synchronized(this) {
		    	if (messages == null)
		    		messages = new ArrayList<RPCMessage>(1);
	    	}
	    	messages.add(message);
	    }
	    private void addAccessLevel(String id, int level) {
	    	List<String> sameLevel =accessLevels[level]; 
	    	if (sameLevel == null)
	    		sameLevel = accessLevels[level] = new ArrayList<String>();
	    	sameLevel.add(id);	    	
	    }
		public JSONObject getAccessLevels() throws JSONException {
			JSONObject jsonAccessLevels = new JSONObject();
			int mostCommonLevel = -1;
			int numberInMostCommon = 0;
			for (int i = 0; i < 6; i++ ) {
				List<String> sameLevel = accessLevels[i];
				if (sameLevel != null && sameLevel.size() > numberInMostCommon) {
					numberInMostCommon = sameLevel.size();
					mostCommonLevel = i;
				}
			}
			if (numberInMostCommon > 0)
				jsonAccessLevels.put("default", mostCommonLevel);
			
			for (int i = 0; i < 6; i++ ) 
				if (i != mostCommonLevel)
				{
					List<String> sameLevel = accessLevels[i];
					if (sameLevel != null) 
						for (String key : sameLevel)
							jsonAccessLevels.put(key, i);
				}
			
			return jsonAccessLevels;
		}
		public PostResponse newPostResponse(String postResponseId) {
			PostResponse postResponse =	new PostResponse();
			postResponses.put(postResponseId, postResponse);
			return postResponse;
		}
		public PostResponse pullPostResponse(String postResponseId) {
			PostResponse postResponse;
			int i = 0;
			do {
				postResponse = postResponses.get(postResponseId); // remove it to clear the memory
				if (postResponse == null || !postResponse.finished)
					try {
						Thread.sleep(50);
					} catch (InterruptedException e) {
						throw new RuntimeException(e);
					}
				if (i++ > 1000)
					throw new RuntimeException("Post response timeout");
			} while(postResponse == null || !postResponse.finished);
			return postResponse;
		}
	}
	
	Map<String,Persistable> clientSideObjects = new HashMap<String,Persistable>(); 
	Map<String,Persistable> clientObjectsChangeIdNeeded = new HashMap<String,Persistable>();
	public Persistable clientSideObject(String id,Persistable newObject) {
		synchronized (clientObjectsChangeIdNeeded) {
			Persistable value = clientSideObjects.get(id);
			if (value != null) {
				clientObjectsChangeIdNeeded.put(id,value);
				return value;
			}
			value = newObject;
			ObjectId.insertObjectForId(id,value);
			clientSideObjects.put(id,value);
			clientObjectsChangeIdNeeded.put(id,value);
			return value;			
		}
	}
	
	
	
	/** This section is for the connection */
    HttpSession session;
    public String mode;
    public String newFileName;
    public boolean refreshRequired = false;
    //public String webappContextPath;
    
    /*public void setWebsiteIdentification(String path) {
        websiteIdentification = path;         
    }*/
    public void setSession(HttpSession session) { 
        this.session = session;
    }
    public HttpSession getSession() {
    	return session;
    }
    public class Sequence {
    	public long seqId;
    	public boolean isExecuting = false;
		public Sequence(long seqId) {
			super();
			this.seqId = seqId;
		}
    }
    List<Sequence> sequences = new ArrayList<Sequence>();
    public Sequence getSequenceObject(long seqId) {
    	for (Sequence sequence : sequences)
    		if (sequence.seqId - 3 < seqId && sequence.seqId + 100 > seqId)
    			return sequence;
    	Sequence sequence = new Sequence(seqId);
    	sequences.add(sequence);
    	return sequence;
    }

    public static Map<Thread,IndividualRequest> threadClientMap = new HashMap<Thread,IndividualRequest>();
    
    /** This registers which website we are using for this particular user, which affects the users data and possibly the user table that is used */
    public static void registerThisConnection(IndividualRequest individualRequest) {
    	threadClientMap .put(Thread.currentThread(),individualRequest);
    }
    
   
    public static IndividualRequest getCurrentObjectResponse() { 
    	return threadClientMap .get(Thread.currentThread());
    }
    List<RPCMessage> messages;

    
    private RPCMessage dispatchNextMessages() {
    	synchronized(this) {
	    	if (messages == null || messages.isEmpty())
	    		return null;
	    	return messages.remove(0);
    	}
    }
    /**
     * This allows a new thread to use an client connection, for the purposes of maintaining security, making RPCs, and so forth
     * @param thread
     */
    public void adoptThread(Thread thread) {
    	threadClientMap.put(thread,new IndividualRequest(getCurrentObjectResponse().locale));
    	//TODO: User registration also
    }
	public String getAuthorization() {
		return authorization;
	}
	public void setAuthorization(String authorization) {
		this.authorization = authorization;
	}
	public boolean isDebugMode() {
		return debugMode;
	}
	public void setDebugMode(boolean debugMode) {
		this.debugMode = debugMode;
	}
	public static void setClientTransformationPlugin(
			ClientTransformationPlugin clientTransformationPlugin) {
		ClientConnection.clientTransformationPlugin = clientTransformationPlugin;
	}

}